Wrangle survey data

parseJSONCol <- function(col) {
  #converts ' to " except when surrounded by alphabetical letters, like "you've"
  col <- str_replace_all(col, "(?<![a-zA-Z])'(.*?)'(?![a-zA-Z])", "\"\\1\"")
  col <- lapply(col, fromJSON)
  return(col)
}

# Apply the transformation to all columns with stringified JSON
tutorial <- tutorial_raw %>%
  mutate(across(4:9, parseJSONCol)) %>%
  pivot_longer(cols = 4:9, names_to = "qlabel") %>%
  unnest_wider(value)


endParse <- function(col) {
  col <- lapply(col, function(c) {strsplit(c, "'a': ")[[1]][2]})
  col <- str_remove_all(col, "\\}")
  return(col)
}
end <- end_raw %>%
  mutate(across(4:6, endParse)) #%>%
  # View()

demo <- demo_raw %>%
  mutate(across(4:10, endParse))

Analyze tutorial survey responses

# Distribution of responses to tutorial questions
tutorial %>%
  mutate(correctans = a == corr) %>%
  ggplot(aes(x=a, fill=correctans)) +
  geom_bar(stat="count") + 
  scale_x_discrete("participant response") +
  scale_fill_manual(values=c("red","forestgreen")) +
  facet_wrap(~q, scales="free_x", nrow=3) +
  theme_bw() +
  theme(strip.text.x = element_text(size = 8))

ggsave("fig/tut_distqs.png", width=9, height=6)

# Distribution of correct responses for participants
passtutorial <- tutorial %>%
  mutate(correctans = a == corr) %>%
  group_by(subjID) %>%
  summarise(tutcorr = sum(correctans))
passtutorial %>%
  ggplot(aes(x=tutcorr)) +
  geom_bar(stat="count") + 
  scale_x_continuous("participant total correct responses") +
  scale_y_continuous(limits=c(0,20)) +
  theme_bw()

ggsave("fig/tut_distcorr.png", width=6, height=4)

Examine tutorial trials

raw %>%
  filter(exptPart == "tutorial") %>%
  count(subjID, numTrial) %>%
  ggplot(aes(x=n)) +
  geom_bar(stat="count") +
  facet_wrap(~numTrial)

completedTut <- raw %>%
  filter(exptPart == "tutorial") %>%
  count(subjID, numTrial) %>%
  complete(subjID, numTrial, fill=list(n=0))

completedTut %>%
  arrange(n, desc(numTrial))
## # A tibble: 102 × 3
##    subjID                                   numTrial     n
##    <chr>                                       <dbl> <int>
##  1 connect-01BDCB71123048979A77AB656EEBD585        2     0
##  2 connect-4DB4741723D94FD2B85BB902808C0730        2     0
##  3 connect-A0D00061575B499A8080DBF3CACA6A08        2     0
##  4 connect-B19D08B7E13D4CBFB503C08C6E51FF96        2     0
##  5 connect-FA3BED3C33C047EAB2768EF5172A1310        2     0
##  6 connect-4DB4741723D94FD2B85BB902808C0730        1     0
##  7 connect-A0D00061575B499A8080DBF3CACA6A08        1     0
##  8 connect-B19D08B7E13D4CBFB503C08C6E51FF96        1     0
##  9 connect-FA3BED3C33C047EAB2768EF5172A1310        1     0
## 10 connect-0F07CDB90B904FB79C385106E46337DE        2     1
## # ℹ 92 more rows
# get participants who didn't finish tutorial
didntfinish <- completedTut %>%
  filter(n == 0) %>%
  distinct(subjID) %>%
  pull(subjID)
didntfinish
## [1] "connect-01BDCB71123048979A77AB656EEBD585"
## [2] "connect-4DB4741723D94FD2B85BB902808C0730"
## [3] "connect-A0D00061575B499A8080DBF3CACA6A08"
## [4] "connect-B19D08B7E13D4CBFB503C08C6E51FF96"
## [5] "connect-FA3BED3C33C047EAB2768EF5172A1310"
strokes %>%
  filter(exptPart == "tutorial" & !subjID %in% didntfinish) %>%
  mutate(editaction = ifelse(action %in% c("clear","undo"), "remove", action),
         editaction = ifelse(editaction == "drawerror", "error", editaction),
         numtrial = as.factor(numtrial),
         subjID = substring(subjID, 9, 18)) %>%
  ggplot(aes(x = editaction, fill = numtrial)) +
  geom_bar(stat="count", position="stack") +
  facet_wrap(~subjID, nrow=5) +
  theme_bw()

ggsave("fig/tutorialstrokes.png", width=10, height=8)
# some participants are missing strokes in tutorial trial 1 -- look up who these are

strokes %>%
  filter(exptPart == "tutorial" & !subjID %in% didntfinish) %>%
  mutate(editaction = ifelse(action %in% c("clear","undo"), "remove", action),
         numtrial = as.factor(numtrial)) %>%
  count(subjID, numtrial) %>% #count number of actions per trial
  complete(subjID, numtrial, fill=list(n=0)) %>%
  filter(n == 0) %>%
  pull(subjID)
## [1] "connect-450BA15B1E9D4B76BC0DCB0D9BF11DAB"
## [2] "connect-4ECCC073A845416680A6078E5A14A8F7"
## [3] "connect-B9360F5511954062A797B1228A4AF9E9"
techIssues <- c("connect-450BA15B1E9D4B76BC0DCB0D9BF11DAB") 
#commented about technical challenges

Filter participants who fail tutorial

failedSubj <- passtutorial %>%
  filter(tutcorr < 4) %>%
  pull(subjID)

data <- raw %>%
  filter(!subjID %in% c(failedSubj, didntfinish, techIssues), exptPart == "test") %>%
  mutate(subjID = substring(subjID, 9, 18))

strokedat <- strokes %>%
  filter(!subjID %in% c(failedSubj, didntfinish, techIssues), exptPart == "test",
         numattempt < 5) %>%
  mutate(subjID = substring(subjID, 9, 18))

Basic info about participants

# number of participants
data %>%
  distinct(subjID) %>%
  nrow()
## [1] 27
# 27 participants -- 1 was removed for failing tutorial survey, 1 had data saving issues, 1 had line drawing issues

# did all participants do all 20 trials
data %>%
  distinct(subjID, numTrial) %>%
  filter(numTrial == 19)
## # A tibble: 27 × 2
##    subjID     numTrial
##    <chr>         <dbl>
##  1 1DF28D04D4       19
##  2 DF011FC195       19
##  3 BDE67C2DF7       19
##  4 A4CF186F80       19
##  5 3816DCE76C       19
##  6 4E48E94501       19
##  7 6F75D8B9C0       19
##  8 71AEE38C92       19
##  9 F9E998D1F7       19
## 10 AD5993CF4D       19
## # ℹ 17 more rows
# all 27 participants completed trials 0 to 19
imageInfo <- raw %>%
  filter(!subjID %in% c(failedSubj, didntfinish, techIssues), exptPart == "test") %>%
  mutate(success = ifelse(runOutcome == "goal", "success", "fail"),
         tempattempt = 1) %>%
  group_by(subjID) %>%
  mutate(cumulativeAttempt = cumsum(tempattempt),
         cumulativeAttempt = cumulativeAttempt - 1,
         imageID = paste0(subjID, "_img", cumulativeAttempt, ".png"),
         directory = paste0(subjID,"/",imageID)) %>%
  select(subjID, levelID, gravX, gravY, radius, numAttempts, success, imageID, directory)
write.csv(imageInfo, "imagedf.csv")

Analysis

number of attempts

## number of total attempts by participants
data %>%
  count(subjID) %>%
  arrange(n)
## # A tibble: 27 × 2
##    subjID         n
##    <chr>      <int>
##  1 A4CF186F80    34
##  2 0F07CDB90B    36
##  3 4E48E94501    36
##  4 4ECCC073A8    37
##  5 4C241572C4    41
##  6 F8D52DE7E5    41
##  7 7E0D9F23EB    43
##  8 C809D394C2    45
##  9 DF011FC195    45
## 10 71AEE38C92    46
## # ℹ 17 more rows
data %>%
  count(subjID) %>%
  ggplot(aes(x=n)) +
  geom_histogram(binwidth=5)

## number of trials/conditions
data %>%
  filter(numAttempts == 0) %>%
  count(levelID, gravX, gravY, radius) %>%
  complete(levelID, gravX, gravY, radius, fill=list(n=0)) %>%
  arrange(n)
## # A tibble: 168 × 5
##    levelID          gravX gravY radius     n
##    <chr>            <dbl> <dbl>  <dbl> <int>
##  1 basic_far         0      2.5     30     0
##  2 basic_short      -0.25   1       30     0
##  3 basic_short       0      1       20     0
##  4 basic_steep       0.25   1       20     0
##  5 block_med         0      1       30     0
##  6 boomerang_left    0      2.5     30     0
##  7 boomerang_left    0.25   1       20     0
##  8 boomerang_middle  0      1       30     0
##  9 contain_corner    0.25   1       20     0
## 10 tunnel_narrow    -0.25   2.5     30     0
## # ℹ 158 more rows
## number of attempts for trials
data %>%
  count(levelID) %>%
  arrange(n)
## # A tibble: 14 × 2
##    levelID              n
##    <chr>            <int>
##  1 basic_short         36
##  2 basic_drop          58
##  3 basic_steep         68
##  4 tunnel_narrow       83
##  5 tunnel_wide         84
##  6 contain_corner      87
##  7 basic_far           91
##  8 diagonal_descent    97
##  9 block_med          112
## 10 boomerang_left     112
## 11 boomerang_middle   120
## 12 basic_flat         140
## 13 block_long         142
## 14 diagonal_ascent    249
data %>%
  count(levelID) %>%
  ggplot(aes(x=reorder(levelID, n), y=n)) +
  geom_bar(stat="identity") +
  scale_x_discrete("level ID") +
  coord_flip() +
  theme_bw()

## of attempts for conditions
data %>%
  count(gravX)
## # A tibble: 3 × 2
##   gravX     n
##   <dbl> <int>
## 1 -0.25   603
## 2  0      458
## 3  0.25   418
data %>%
  count(gravX) %>%
  mutate(gravX = as.factor(gravX)) %>%
  ggplot(aes(x=gravX, y=n, fill=gravX)) +
  geom_bar(stat="identity", colour="black", show.legend=FALSE) +
  geom_text(aes(label = n), position = position_dodge(0.9), vjust = -0.2) +
  scale_x_discrete("Wind") +
  scale_y_continuous("Number of Attempts") +
  theme_bw()

## number of attempts for trials/conditions
data %>%
  count(levelID) %>%
  ggplot(aes(x=reorder(levelID, n), y=n)) +
  geom_bar(stat="identity") +
  scale_x_discrete("level ID") +
  coord_flip() +
  theme_bw()

categorize successful vs failed trials

## breakdown of outcome for each participant
data %>%
  count(subjID, runOutcome)
## # A tibble: 81 × 3
##    subjID     runOutcome     n
##    <chr>      <chr>      <int>
##  1 0F07CDB90B goal          18
##  2 0F07CDB90B outofbound     5
##  3 0F07CDB90B stationary    13
##  4 1304BAAAAF goal          13
##  5 1304BAAAAF outofbound     6
##  6 1304BAAAAF stationary    34
##  7 1501C5CE8B goal          12
##  8 1501C5CE8B outofbound    20
##  9 1501C5CE8B stationary    32
## 10 1A8CD277EE goal          17
## # ℹ 71 more rows
ggplot(data, aes(x=runOutcome)) +
  geom_bar(stat="count") +
  facet_wrap(~subjID) +
  theme_bw() +
  theme(axis.text.x = element_text(angle=90, vjust=0, hjust=1))

## proportion of success for each participant
data %>%
  count(subjID, runOutcome) %>%
  filter(runOutcome == "goal") %>%
  mutate(propsuccess = n/20) %>%
  ggplot(aes(x = reorder(subjID, -propsuccess), y = propsuccess)) +
  geom_bar(stat="identity") +
  scale_x_discrete("subject ID") +
  scale_y_continuous(limits=c(0,1), breaks=seq(0,1,0.2), expand=c(0,0)) +
  coord_flip() +
  theme_bw()

ggsave("fig/subjSuccess.png", width=3, height=6)

number of successful attempts by condition multiplot

data %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(gravX, outcome) %>%
  mutate(gravX = as.factor(gravX)) %>%
  ggplot(aes(x=gravX, y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="black") +
  scale_x_discrete("Wind") +
  scale_y_continuous("Number of Attempts") +
  scale_fill_manual(values=c("red","forestgreen")) +
  theme_bw()

ggsave("fig/attByGravX.png", width=7, height=3)

data %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(gravY, outcome) %>%
  mutate(gravY = as.factor(gravY)) %>%
  ggplot(aes(x=gravY, y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="blue") +
  scale_x_discrete("Planet") +
  scale_y_continuous("Number of Attempts") +
  scale_fill_manual(values=c("red","forestgreen")) +
  theme_bw()

ggsave("fig/attByGravY.png", width=7, height=3)

data %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(radius, outcome) %>%
  mutate(radius = as.factor(radius)) %>%
  ggplot(aes(x=radius, y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="orange") +
  scale_x_discrete("Marble Size") +
  scale_y_continuous("Number of Attempts") +
  scale_fill_manual(values=c("red","forestgreen")) +
  theme_bw()

ggsave("fig/attBySize.png", width=7, height=3)

data %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(levelID, outcome) %>%
  group_by(levelID) %>%
  mutate(sum = sum(n)) %>%
  ggplot(aes(x=reorder(levelID, sum), y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="gray") +
  scale_x_discrete("Trial") +
  scale_y_continuous("Number of Attempts") +
  scale_fill_manual(values=c("red","forestgreen")) +
  coord_flip() +
  theme_bw()

ggsave("fig/attByTrial.png", width=7, height=7)

combining trial and gravX

data %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(gravX, levelID, outcome) %>%
  group_by(gravX, levelID) %>%
  mutate(sum = sum(n),
         gravX = paste("wind =", gravX)) %>%
  ggplot(aes(x=reorder(levelID, sum), y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="gray") +
  scale_x_discrete("Trial") +
  scale_y_continuous("Number of Attempts", limits=c(0,110), breaks=seq(0,110, 25)) +
  scale_fill_manual(values=c("red","forestgreen")) +
  coord_flip() +
  facet_wrap(~gravX) +
  theme_bw()

ggsave("fig/attByTrialGravX.png", width=8, height=4)
data %>%
  filter(levelID %in% c("diagonal_ascent", "basic_flat", "block_long", "boomerang_middle")) %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(gravY, radius, levelID, outcome) %>%
  group_by(gravY, radius, levelID) %>%
  mutate(sum = sum(n)) %>%
  ggplot(aes(x=reorder(levelID, sum), y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="gray") +
  scale_x_discrete("Trial") +
  scale_y_continuous("Number of Attempts", limits=c(0,110), breaks=seq(0,110, 25)) +
  scale_fill_manual(values=c("red","forestgreen")) +
  coord_flip() +
  facet_grid(gravY~radius) +
  theme_bw()

data %>%
  filter(gravY == 2.5 & radius == 20) %>%
  mutate(outcome = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(gravX, levelID, outcome) %>%
  group_by(gravX, levelID) %>%
  mutate(sum = sum(n),
         gravX = paste("wind =", gravX)) %>%
  ggplot(aes(x=reorder(levelID, sum), y=n, fill=outcome)) +
  geom_bar(stat="identity", colour="gray") +
  scale_x_discrete("Trial") +
  scale_y_continuous("Number of Attempts", limits=c(0,110), breaks=seq(0,110, 25)) +
  scale_fill_manual(values=c("red","forestgreen")) +
  coord_flip() +
  facet_wrap(~gravX) +
  theme_bw()

Learning over attempts

maxAttempts <- data %>%
  filter(numAttempts == 0) %>%
  count(levelID, gravX) %>%
  rename(maxAttempts = n)

# Number of Attempts
data %>%
  count(levelID, gravX, numAttempts) %>%
  complete(levelID, gravX, numAttempts, fill=list(n=0)) %>%
  mutate(numAttempts = as.factor(numAttempts),
         gravX = paste("wind =", gravX)) %>%
  ungroup() %>%
  group_by(levelID, gravX) %>%
  mutate(sum = sum(n)) %>%
  ggplot(aes(x = reorder(levelID, sum), y=n, fill=numAttempts)) +
  geom_bar(stat="identity", position=position_dodge2(preserve = "single", reverse = TRUE)) +
  scale_x_discrete("Trial") +
  facet_wrap(~gravX) +
  coord_flip() +
  theme_bw()

ggsave("fig/survival.png", width=8, height=4)

# Proportion of Success over attempts
data %>%
  mutate(success = ifelse(runOutcome == "goal", "success", "fail")) %>%
  count(levelID, gravX, numAttempts, success) %>%
  complete(levelID, gravX, numAttempts, success, fill=list(n=0)) %>%
  filter(success == "success") %>%
  left_join(maxAttempts, by=c("levelID","gravX")) %>%
  mutate(gravX = paste("wind =", gravX)) %>%
  group_by(levelID, gravX) %>%
  mutate(cumsum = cumsum(n),
         propSuccess = cumsum/maxAttempts,
         numAttempts = numAttempts + 1) %>%
  ggplot(aes(x = numAttempts, y=propSuccess)) +
  geom_line(stat="identity", colour="blue", size=1.5) +
  scale_x_continuous("Attempts") +
  scale_y_continuous("Proportion of Success", limits=c(0,1), expand=c(0,0)) +
  facet_grid(gravX~levelID) +
  theme_bw()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

ggsave("fig/learning.png", width=12, height=4)

Learning over trials

# proportion of successful attempts
data %>%
  mutate(success = ifelse(runOutcome == "goal", "success", "fail"),
         numSucc = as.numeric(success == "success")) %>%
  ggplot(aes(x=numTrial, y=numSucc)) +
  geom_point(stat="summary") +
  geom_errorbar(stat="summary") +
  geom_smooth() +
  theme_bw()
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

# proportion successful trials
trialSuccess <- data %>%
  group_by(subjID, numTrial, levelID, gravX, gravY, radius) %>%
  summarise(success = ifelse("goal" %in% runOutcome, "success", "fail")) %>%
  mutate(numSucc = as.numeric(success=="success"))
## `summarise()` has grouped output by 'subjID', 'numTrial', 'levelID', 'gravX',
## 'gravY'. You can override using the `.groups` argument.
trialSuccess %>%
  ggplot(aes(x=numTrial, y=numSucc)) +
  geom_point(stat="summary") +
  geom_errorbar(stat="summary") +
  geom_smooth() +
  scale_x_continuous("Trial Number") +
  scale_y_continuous("Proportion of Success") +
  theme_bw()
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

ggsave("fig/learningTrials.png", width=7, height=4)
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

number of actions

# mean of each action per attempt
strokedat %>%
  group_by(subjID, numtrial, numattempt, action) %>%
  count() %>%
  group_by(action) %>%
  summarise(mean = mean(n))
## # A tibble: 3 × 2
##   action  mean
##   <chr>  <dbl>
## 1 add     2.00
## 2 clear   2.17
## 3 undo    1.57
# mean of all actions per attempt
strokedat %>%
  group_by(subjID, numtrial, numattempt) %>%
  count() %>%
  ungroup() %>%
  summarise(mean = mean(n))
## # A tibble: 1 × 1
##    mean
##   <dbl>
## 1  3.13
strokedat %>%
  group_by(subjID, numtrial, numattempt, action) %>%
  count() %>%
  ggplot(aes(x=numattempt, y=n, fill=action)) +
  geom_bar(stat = "summary", position=position_dodge()) +
  geom_errorbar(stat = "summary", position=position_dodge()) +
  scale_x_continuous("attempt number") +
  scale_y_continuous("number of action") +
  theme_bw()
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`

ggsave("fig/strokebyattempt.png", width=7, height=5)
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`
strokedat %>%
  group_by(subjID, numtrial, numattempt, action) %>%
  count() %>%
  ggplot(aes(x=numtrial, y=n, fill=action)) +
  geom_bar(stat = "summary", position=position_dodge()) +
  geom_errorbar(stat = "summary", position=position_dodge())
## No summary function supplied, defaulting to `mean_se()`
## No summary function supplied, defaulting to `mean_se()`

data %>%
  mutate(logDrawTime = log(drawTime)) %>%
  ggplot(aes(x=numTrial, y=logDrawTime)) +
  geom_point()

ggsave("fig/drawtime.png", width=7, height=3)

data %>%
  mutate(logRunTime = log(runTime)) %>%
  ggplot(aes(x=numTrial, y=logRunTime)) +
  geom_point()

ggsave("fig/runtime.png", width=7, height=3)

Individual participants’ time to trial success

data %>%
  mutate(logDrawTime = log(drawTime)) %>%
  ggplot(aes(x=numTrial, y=logDrawTime, colour=subjID)) +
  geom_smooth(se=F)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

data %>%
  mutate(logDrawTime = log(drawTime)) %>%
  ggplot(aes(x=numTrial, y=runTime, colour=subjID)) +
  geom_smooth(se=F)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

data %>%
  count(subjID, numTrial) %>%
  ggplot(aes(x=numTrial, y=n, colour=subjID)) +
  geom_smooth(se=F)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

IQR = quantile(data$drawTime, .75) - quantile(data$drawTime, .25)
lowerbound = quantile(data$drawTime, .25) - 1.5*IQR
upperbound = quantile(data$drawTime, .75) + 1.5*IQR


data %>%
  filter(drawTime < upperbound) %>%
  ggplot(aes(x=numTrial, y=drawTime)) +
  geom_smooth() +
  scale_y_continuous("drawing time")
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

data %>%
  count(subjID, numTrial) %>%
  ggplot(aes(x=numTrial, y=n)) +
  geom_smooth() +
  scale_y_continuous("number of attempts")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

# total trial time %>%
data %>%
  group_by(subjID, numTrial) %>%
  summarise(totalDrawTime = sum(drawTime),
            totalRunTime = sum(runTime)) %>%
  mutate(totalTime = totalDrawTime + totalRunTime) %>%
  ggplot(aes(x=numTrial, y=totalRunTime)) +
  geom_smooth()
## `summarise()` has grouped output by 'subjID'. You can override using the
## `.groups` argument.
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

parseJSON <- function(col) {
  #converts ' to " except when surrounded by alphabetical letters, like "you've"
  col <- str_remove_all(col, "\\[")
  col <- str_remove_all(col, "\\]")
  col <- str_replace_all(col, "(?<![a-zA-Z])'(.*?)'(?![a-zA-Z])", "\"\\1\"")
  col <- lapply(col, fromJSON)
  return(col)
}

data %>%
  mutate(drawnLines = across(c("drawnLines"), parseJSONCol))
## # A tibble: 1,479 × 26
##    subjID         starttime useragent       exptPart numTrial levelIndex levelID
##    <chr>              <dbl> <chr>           <chr>       <dbl>      <dbl> <chr>  
##  1 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            0          0 basic_…
##  2 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            0          0 basic_…
##  3 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            0          0 basic_…
##  4 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            1         13 boomer…
##  5 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            1         13 boomer…
##  6 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            1         13 boomer…
##  7 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            1         13 boomer…
##  8 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            1         13 boomer…
##  9 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            2          9 diagon…
## 10 1DF28D04D4 1703865018453 Mozilla/5.0 (W… test            2          9 diagon…
## # ℹ 1,469 more rows
## # ℹ 19 more variables: marbleStartLoc <chr>, cupLoc <chr>, blockLoc <chr>,
## #   gravX <dbl>, gravY <dbl>, mass <dbl>, radius <dbl>, numAttempts <dbl>,
## #   maxAttempt <dbl>, marbleEndLoc <chr>, marbleCoords <chr>,
## #   marbleEndDist <dbl>, marbleMinDist <dbl>, runOutcome <chr>,
## #   drawnLines <tibble[,1]>, drawnPhysObj <chr>, trialStartTime <dbl>,
## #   drawTime <dbl>, runTime <dbl>